"""
Error handling utilities for graceful error recovery and user notification.

This module provides centralized error handling functionality including
error logging, user notification, and recovery strategies.
"""

import logging
import traceback
from typing import Optional, Callable, Any, Dict, List
from functools import wraps
from pathlib import Path

from .exceptions import (
    PreprocessingPipelineError, 
    FileOperationError, 
    ConfigurationError,
    ProcessingError,
    OutputError,
    UserCancellationError
)


class ErrorHandler:
    """
    Centralized error handler for the preprocessing pipeline.
    
    This class provides methods for handling different types of errors,
    logging them appropriately, and providing user-friendly notifications.
    """
    
    def __init__(self, logger_name: str = "preprocessing_pipeline"):
        """
        Initialize the error handler.
        
        Args:
            logger_name (str): Name for the logger instance
        """
        self.logger = logging.getLogger(logger_name)
        self.error_counts = {}
        self.recent_errors = []
        self.max_recent_errors = 50
        
        # Configure logger if not already configured
        if not self.logger.handlers:
            self._configure_logger()
    
    def _configure_logger(self) -> None:
        """Configure the logger with appropriate handlers and formatters."""
        # Create console handler
        console_handler = logging.StreamHandler()
        console_handler.setLevel(logging.WARNING)
        
        # Create file handler for error logs
        try:
            log_dir = Path("logs")
            log_dir.mkdir(exist_ok=True)
            
            file_handler = logging.FileHandler(log_dir / "errors.log")
            file_handler.setLevel(logging.ERROR)
            
            # Create formatters
            console_formatter = logging.Formatter(
                '%(levelname)s: %(message)s'
            )
            file_formatter = logging.Formatter(
                '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
            )
            
            console_handler.setFormatter(console_formatter)
            file_handler.setFormatter(file_formatter)
            
            self.logger.addHandler(console_handler)
            self.logger.addHandler(file_handler)
            
        except Exception as e:
            # Fallback to console only if file logging fails
            console_handler.setFormatter(logging.Formatter(
                '%(asctime)s - %(levelname)s - %(message)s'
            ))
            self.logger.addHandler(console_handler)
            print(f"Warning: Could not set up file logging: {e}")
        
        self.logger.setLevel(logging.INFO)
    
    def handle_error(self, error: Exception, context: Optional[Dict[str, Any]] = None,
                    user_message: Optional[str] = None, 
                    recovery_action: Optional[Callable] = None) -> bool:
        """
        Handle an error with logging, user notification, and optional recovery.
        
        Args:
            error (Exception): The exception that occurred
            context (Optional[Dict]): Additional context information
            user_message (Optional[str]): Custom user-friendly message
            recovery_action (Optional[Callable]): Function to attempt recovery
            
        Returns:
            bool: True if error was handled successfully, False otherwise
        """
        try:
            # Log the error
            self._log_error(error, context)
            
            # Track error statistics
            self._track_error(error)
            
            # Determine user message
            if user_message is None:
                user_message = format_error_for_user(error)
            
            # Display user notification
            self._notify_user(error, user_message)
            
            # Attempt recovery if provided and error is recoverable
            if recovery_action and is_recoverable_error(error):
                try:
                    self.logger.info("Attempting error recovery...")
                    recovery_result = recovery_action()
                    if recovery_result:
                        self.logger.info("Error recovery successful")
                        return True
                    else:
                        self.logger.warning("Error recovery failed")
                except Exception as recovery_error:
                    self.logger.error(f"Recovery action failed: {recovery_error}")
            
            return False
            
        except Exception as handler_error:
            # If error handling itself fails, log to console
            print(f"Critical: Error handler failed: {handler_error}")
            print(f"Original error: {error}")
            return False
    
    def _log_error(self, error: Exception, context: Optional[Dict[str, Any]] = None) -> None:
        """
        Log an error with appropriate level and detail.
        
        Args:
            error (Exception): The exception to log
            context (Optional[Dict]): Additional context information
        """
        error_type = type(error).__name__
        error_message = str(error)
        
        # Build log message
        log_parts = [f"{error_type}: {error_message}"]
        
        if context:
            context_str = ", ".join([f"{k}={v}" for k, v in context.items()])
            log_parts.append(f"Context: {context_str}")
        
        # Add custom error details if available
        if isinstance(error, PreprocessingPipelineError):
            if error.details:
                details_str = ", ".join([f"{k}={v}" for k, v in error.details.items()])
                log_parts.append(f"Details: {details_str}")
        
        log_message = " | ".join(log_parts)
        
        # Determine log level based on error type
        if isinstance(error, (SystemError, OutputError)):
            self.logger.error(log_message)
            # Also log stack trace for critical errors
            self.logger.debug(traceback.format_exc())
        elif isinstance(error, (ProcessingError, FileError)):
            self.logger.warning(log_message)
        elif isinstance(error, (ConfigurationError, UIError)):
            self.logger.info(log_message)
        else:
            self.logger.error(log_message)
            self.logger.debug(traceback.format_exc())
    
    def _track_error(self, error: Exception) -> None:
        """
        Track error statistics for monitoring and reporting.
        
        Args:
            error (Exception): The exception to track
        """
        error_type = type(error).__name__
        self.error_counts[error_type] = self.error_counts.get(error_type, 0) + 1
        
        # Add to recent errors list
        error_info = {
            'type': error_type,
            'message': str(error),
            'timestamp': logging.Formatter().formatTime(logging.LogRecord(
                name='', level=0, pathname='', lineno=0, msg='', args=(), exc_info=None
            ))
        }
        
        if isinstance(error, PreprocessingPipelineError):
            error_info.update(error.to_dict())
        
        self.recent_errors.append(error_info)
        
        # Keep only recent errors
        if len(self.recent_errors) > self.max_recent_errors:
            self.recent_errors = self.recent_errors[-self.max_recent_errors:]
    
    def _notify_user(self, error: Exception, message: str) -> None:
        """
        Notify the user about an error in a user-friendly way.
        
        Args:
            error (Exception): The exception that occurred
            message (str): User-friendly error message
        """
        # Determine notification level based on error type
        if isinstance(error, UserCancellationError):
            print(f"ℹ️  {message}")
        elif isinstance(error, (ConfigurationError, UIError)):
            print(f"⚠️  {message}")
        elif isinstance(error, (FileError, ProcessingError)):
            print(f"❌ {message}")
        elif isinstance(error, (SystemError, OutputError)):
            print(f"🚨 {message}")
        else:
            print(f"❌ {message}")
        
        # Provide recovery suggestions for common errors
        self._provide_recovery_suggestions(error)
    
    def _provide_recovery_suggestions(self, error: Exception) -> None:
        """
        Provide recovery suggestions based on error type.
        
        Args:
            error (Exception): The exception to provide suggestions for
        """
        suggestions = []
        
        if isinstance(error, FileError):
            suggestions.extend([
                "• Check that the file exists and is accessible",
                "• Verify file permissions",
                "• Ensure the file is not open in another application"
            ])
        
        elif isinstance(error, ConfigurationError):
            suggestions.extend([
                "• Review your configuration settings",
                "• Check that all required fields are filled",
                "• Verify that values are within acceptable ranges"
            ])
        
        elif isinstance(error, ProcessingError):
            suggestions.extend([
                "• Try processing with fewer combinations",
                "• Check your input data for issues",
                "• Verify that all preprocessing methods are working correctly"
            ])
        
        elif isinstance(error, OutputError):
            suggestions.extend([
                "• Check available disk space",
                "• Verify write permissions to the output directory",
                "• Ensure the output directory is not read-only"
            ])
        
        elif isinstance(error, SystemError):
            suggestions.extend([
                "• Close other applications to free up resources",
                "• Try processing smaller datasets",
                "• Restart the application if the problem persists"
            ])
        
        if suggestions:
            print("\nSuggestions:")
            for suggestion in suggestions:
                print(f"  {suggestion}")
    
    def get_error_summary(self) -> Dict[str, Any]:
        """
        Get a summary of all errors encountered.
        
        Returns:
            Dict[str, Any]: Error summary with counts and recent errors
        """
        return {
            'total_errors': sum(self.error_counts.values()),
            'error_counts': self.error_counts.copy(),
            'recent_errors': self.recent_errors.copy(),
            'most_common_error': max(self.error_counts.items(), key=lambda x: x[1])[0] if self.error_counts else None
        }
    
    def clear_error_history(self) -> None:
        """Clear error tracking history."""
        self.error_counts.clear()
        self.recent_errors.clear()
        self.logger.info("Error history cleared")


# Global error handler instance
_global_error_handler = None


def get_error_handler() -> ErrorHandler:
    """
    Get the global error handler instance.
    
    Returns:
        ErrorHandler: The global error handler
    """
    global _global_error_handler
    if _global_error_handler is None:
        _global_error_handler = ErrorHandler()
    return _global_error_handler


def handle_error(error: Exception, context: Optional[Dict[str, Any]] = None,
                user_message: Optional[str] = None, 
                recovery_action: Optional[Callable] = None) -> bool:
    """
    Convenience function to handle an error using the global error handler.
    
    Args:
        error (Exception): The exception that occurred
        context (Optional[Dict]): Additional context information
        user_message (Optional[str]): Custom user-friendly message
        recovery_action (Optional[Callable]): Function to attempt recovery
        
    Returns:
        bool: True if error was handled successfully, False otherwise
    """
    return get_error_handler().handle_error(error, context, user_message, recovery_action)


# Decorator for automatic error handling

def with_error_handling(user_message: Optional[str] = None, 
                       recovery_action: Optional[Callable] = None,
                       reraise: bool = False):
    """
    Decorator to automatically handle errors in functions.
    
    Args:
        user_message (Optional[str]): Custom user message for errors
        recovery_action (Optional[Callable]): Recovery function to attempt
        reraise (bool): Whether to reraise the exception after handling
        
    Returns:
        Decorator function
    """
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            try:
                return func(*args, **kwargs)
            except Exception as e:
                context = {
                    'function': func.__name__,
                    'args_count': len(args),
                    'kwargs_keys': list(kwargs.keys())
                }
                
                handled = handle_error(e, context, user_message, recovery_action)
                
                if reraise or not handled:
                    raise
                
                return None
        return wrapper
    return decorator


# Validation decorators with error handling

def validate_input(validation_func: Callable, error_message: str):
    """
    Decorator to validate function inputs and raise appropriate errors.
    
    Args:
        validation_func (Callable): Function to validate inputs
        error_message (str): Error message if validation fails
        
    Returns:
        Decorator function
    """
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            try:
                if not validation_func(*args, **kwargs):
                    from .exceptions import UserInputError
                    raise UserInputError(
                        input_type=func.__name__ + "_input",
                        input_value=f"args={args}, kwargs={kwargs}",
                        expected_format="valid input parameters",
                        message=error_message
                    )
                return func(*args, **kwargs)
            except Exception as e:
                if not isinstance(e, PreprocessingPipelineError):
                    # Wrap non-custom exceptions
                    from .exceptions import PreprocessingPipelineError
                    wrapped_error = PreprocessingPipelineError(
                        f"Error in {func.__name__}: {str(e)}",
                        details={'function': func.__name__, 'original_error': str(e)}
                    )
                    raise wrapped_error from e
                raise
        return wrapper
    return decorator


# Context manager for error handling

class ErrorContext:
    """
    Context manager for handling errors within a specific context.
    """
    
    def __init__(self, operation_name: str, 
                 user_message: Optional[str] = None,
                 recovery_action: Optional[Callable] = None,
                 suppress_errors: bool = False):
        """
        Initialize the error context.
        
        Args:
            operation_name (str): Name of the operation being performed
            user_message (Optional[str]): Custom user message for errors
            recovery_action (Optional[Callable]): Recovery function to attempt
            suppress_errors (bool): Whether to suppress errors (return None instead of raising)
        """
        self.operation_name = operation_name
        self.user_message = user_message
        self.recovery_action = recovery_action
        self.suppress_errors = suppress_errors
        self.error_handler = get_error_handler()
    
    def __enter__(self):
        return self
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        if exc_type is not None:
            context = {'operation': self.operation_name}
            
            handled = self.error_handler.handle_error(
                exc_val, context, self.user_message, self.recovery_action
            )
            
            if self.suppress_errors or handled:
                return True  # Suppress the exception
        
        return False  # Let the exception propagate